#include "Socket.h"
#include "SocketException.h"
#include <sys/socket.h>
#include <sys/unistd.h>
#include <sys/errno.h>
#include <sys/time.h>

Socket::Socket(TransportProtocol transportProtocol)
{
    initialize();
    
    this->transportProtocol = transportProtocol;
    
    execCreateSocket();
}

void Socket::initialize()
{
    transportProtocol = UNSPECIFIED;
    socketDescriptor = CONST_UNINITIALIZED_SOCKET;
    timeout = CONST_NO_TIMEOUT;
    connected = false;
    bound = false;
    closed = false;    
}
    
bool Socket::socketInitialized()
{
    return (socketDescriptor != CONST_UNINITIALIZED_SOCKET);
}

void Socket::execCreateSocket()
{
    int functionResult;
    int socketType;
    
    switch(transportProtocol)
    {
        case TCP:
            socketType = SOCK_STREAM;
            break;
            
        case UDP:
            socketType = SOCK_DGRAM;
            break;
    }
    
    functionResult = socket(AF_INET, socketType, 0);
    
    if(functionResult > -1)
    {
        socketDescriptor = functionResult;
    }
    else
    {
        handleCreateSocketError(errno);
    }
}

void Socket::handleCreateSocketError(int errorCode)
{
    int exceptionCode;
    
    switch(errorCode)
    {
        case EMFILE:
        case ENFILE:
        case ENOBUFS:
        case ENOMEM:
            exceptionCode = SocketException::ERROR_SYSTEM;
            break;
            
        case EACCES:
            exceptionCode = SocketException::ERROR_ACCESS_DENIED;
            break;
            
        default:
            exceptionCode = SocketException::ERROR_SOCKET_ERROR;
    };
    
    throw SocketException(exceptionCode);
}

void Socket::execBind(const SocketEndPoint & localEndPoint)
{
    struct sockaddr_in localSockAddr = {};
    int bindResult;
    int errorCode;
    
    initializeSockAddrInFromSocketEndPoint(localEndPoint, &localSockAddr);
    
    bindResult = bind(socketDescriptor, (struct sockaddr *) &localSockAddr, sizeof(struct sockaddr_in));
    
    if(bindResult != 0)
    {
        handleBindError(errno);
    }
}
 
void Socket::handleBindError(int errorCode)
{
    switch(errorCode)
        {
            case EADDRINUSE:
                errorCode = SocketException::ERROR_BIND_ADDRESS_ALREADY_USED;
                break;

            case EADDRNOTAVAIL:
                errorCode = SocketException::ERROR_BIND_ADDRESS_UNAVAILABLE;
                break;

            case EACCES:
                errorCode = SocketException::ERROR_ACCESS_DENIED;
                break;

            default:
                errorCode = SocketException::ERROR_SOCKET_ERROR;
                break;
        };
        
        throw SocketException(errorCode);
}

void Socket::bindSocket(const SocketEndPoint & localEndPoint)
{
    if(bound)
    {
        throw SocketException(SocketException::ERROR_SOCKET_ALREADY_BOUNDED);
    }
    else
    {
        execBind(localEndPoint);
    }
    
    this->localEndPoint = localEndPoint;
    bound = true;
}

void Socket::closeSocket()
{
    if(!socketInitialized())
    {
        throw SocketException(SocketException::ERROR_SOCKET_UNINITIALIZED);
    }
    
    shutdown(socketDescriptor, SHUT_RDWR);
    close(socketDescriptor);
    
    socketDescriptor = CONST_UNINITIALIZED_SOCKET;
    bound = false;
    connected = false;
    closed = true;
}

void Socket::initializeSockAddrInFromSocketEndPoint(
        const SocketEndPoint& socketEndPoint, struct sockaddr_in* sockAddr)
{
    sockAddr->sin_family = AF_INET;
    sockAddr->sin_addr.s_addr = socketEndPoint.getIpAddress().getIpAddressInNetworkOrder();
    sockAddr->sin_port = htons(socketEndPoint.getPort());
}

void Socket::connectToHost(const SocketEndPoint & remoteEndPoint)
{
    int functionResult;
    struct sockaddr_in remoteSockAddr = {};
    
    throwExceptionIfSocketNotInitialized();
    
    if(connected)
    {
        throw SocketException(SocketException::ERROR_SOCKET_ALREADY_CONNECTED);
    }
    
    initializeSockAddrInFromSocketEndPoint(remoteEndPoint, &remoteSockAddr);
    
    functionResult = connect(socketDescriptor, (struct sockaddr *) &remoteSockAddr, sizeof(sockaddr_in));
    
    if(functionResult < 0)
    {
        handleConnectError(errno);
    }
    
    this->remoteEndPoint = remoteEndPoint;
    bound = true;
    connected = true;
}

void Socket::handleConnectError(int errorCode)
{
    int exceptionCode;
    
    switch(errorCode)
    {
        case ECONNREFUSED:
        case ECONNRESET:
            exceptionCode = SocketException::ERROR_CONNECTION_REFUSED;
            break;
        
        case EHOSTUNREACH:
            exceptionCode = SocketException::ERROR_HOST_UNREACHABLE;
            break;
            
        default:
            exceptionCode = SocketException::ERROR_SYSTEM;
    };
    
    throw SocketException(errorCode);
}

void Socket::throwExceptionIfReadWritePreConditionsFail()
{
    if(!connected)
    {
        throw SocketException(SocketException::ERROR_SOCKET_UNCONNECTED);
    }
    
    throwExceptionIfSocketClosed();
}

void Socket::throwExceptionIfArgumentNegative(int length)
{
    if(length < 0)
    {
        SocketException ex(SocketException::ERROR_ARGUMENT);
        ex.setDescription("Length argument cannot be negative.");
        
        throw ex;
    }
}

void Socket::handleReadWriteError(int errorCode)
{
    int exceptionCode;
    
    switch(errorCode)
    {
        case EIO:
        case ENOBUFS:
            exceptionCode = SocketException::ERROR_SYSTEM;
            break;
            
        case EACCES:
            exceptionCode = SocketException::ERROR_ACCESS_DENIED;
            break;
            
        case ENETDOWN:
            exceptionCode = SocketException::ERROR_NETWORK;
            break;
            
        case ENETUNREACH:
            exceptionCode = SocketException::ERROR_HOST_UNREACHABLE;
            break;
            
        default:
            exceptionCode = SocketException::ERROR_SYSTEM;
    };
    
    throw SocketException(errorCode);
}

int Socket::writeToSocket(void * buffer, int length)
{
    int functionResult = 0;

    throwExceptionIfArgumentNegative(length);
    throwExceptionIfReadWritePreConditionsFail();   
    functionResult = write(socketDescriptor, buffer, length);
    
    if(functionResult < 0)
    {
        handleReadWriteError(errno);
    }
    
    return functionResult;
}

int Socket::readFromSocket(void * buffer, int length)
{
    int functionResult = 0;
    
    throwExceptionIfArgumentNegative(length);
    throwExceptionIfReadWritePreConditionsFail();
    functionResult = read(socketDescriptor, buffer, length);
    
    if(functionResult < 0)
    {
        handleReadWriteError(errno);
    }
    
    return functionResult;
}

const SocketEndPoint & Socket::getLocalEndPoint() const
{
    if(!bound)
    {
        throw SocketException(SocketException::ERROR_SOCKET_UNBOUNDED);
    }
    else
    {
        return localEndPoint;
    }
}

const SocketEndPoint & Socket::getRemoteEndPoint() const
{
    if(!connected)
    {
        throw SocketException(SocketException::ERROR_SOCKET_UNCONNECTED);
    }
    else
    {
        return remoteEndPoint;
    }
}

void Socket::setReadTimeout(int timeout)
{
    throwExceptionIfArgumentNegative(timeout);
    throwExceptionIfSocketNotInitialized();
    
    struct timeval timeoutTimeval = {};
    timeoutTimeval.tv_sec = timeout;
    
    setsockopt(socketDescriptor, SOL_SOCKET, SO_RCVTIMEO, (void *) &timeoutTimeval, sizeof(struct timeval));
    
    this->timeout = timeout;
}

int Socket::getReadTimeout() const
{
    return timeout;
}

TransportProtocol Socket::getTransportProtocol() const
{
    return transportProtocol;
}

bool Socket::isConnected() const
{
    return connected;
}

bool Socket::isBound() const
{
    return bound;
}

void Socket::throwExceptionIfSocketClosed()
{
    if(closed)
    {
        throw SocketException(SocketException::ERROR_SOCKET_CLOSED);
    }
}

void Socket::throwExceptionIfSocketNotInitialized()
{
    if(!socketInitialized())
    {
        throw SocketException(SocketException::ERROR_SOCKET_UNINITIALIZED);
    }
}



